package com.tao.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.tao.base.dictionary.FileType;
import com.tao.base.dictionary.ObjectAuditStatus;
import com.tao.base.exception.LearnHubException;
import com.tao.base.model.PageParams;
import com.tao.base.model.PageResult;
import com.tao.media.domain.dto.QueryMediaParamsDto;
import com.tao.media.domain.dto.UploadFilesDto;
import com.tao.media.domain.pojo.MediaFiles;
import com.tao.media.domain.pojo.MediaProcess;
import com.tao.media.mapper.MediaFilesMapper;
import com.tao.media.service.IMediaFilesService;
import com.tao.media.domain.vo.UploadFilesVo;
import io.minio.*;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * 媒资信息 服务实现类
 * </p>
 *
 * @author taohongrun
 * @since 2024-11-05
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements IMediaFilesService {


    @Autowired
     MinioClient minioClient;

    @Autowired
    MediaFilesMapper mediaFilesMapper;

    @Autowired
    MediaFilesServiceImpl mediaFilesServiceSelf;

    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;

    }

    //普通文件桶
    @Value("${minio.bucket.files}")
    private String bucketFiles;

    //视频文件桶
    @Value("${minio.bucket.video}")
    private String bucketVideo;

    //获取文件默认存储目录路径 年/月/日
    private String getDefaultObjectPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String folder = sdf.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    private String getBlockObjectPath(String md5){
        return md5.charAt(0)+"/"+md5.charAt(1)+"/"+md5+"/chunk/";
    }

    private String getDefaultObjectPath(String md5){
        return md5.charAt(0)+"/"+md5.charAt(1)+"/"+md5;
    }

    private String getMergeObject(String md5,String extension){
        return md5.charAt(0)+"/"+md5.charAt(1)+"/"+md5+'/'+md5+extension;
    }


    //获取文件的md5
    public String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            String fileMd5 = DigestUtils.md5Hex(fileInputStream);
            return fileMd5;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private String getMimeType(String extension) {
        if (extension == null)
            extension = "";
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        //通用mimeType，字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }



    public void uploadFilesToMinio(String localFilePath, String mimeType, String bucket, String objectName) throws Exception {
        minioClient.uploadObject(UploadObjectArgs.builder()
                .bucket(bucket)
                .filename(localFilePath)
                .object(objectName)
                .contentType(mimeType)
                .build());
    }

    private GetObjectResponse getObjectFromMinio(String bucketName,String objectName){
        try {
             return minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private MediaFiles uploadFilesToDb(UploadFilesDto uploadFilesDto,Long companyId,String fileMd5,String bucket,String object) {
        MediaFiles mediaFiles = new MediaFiles();
        BeanUtils.copyProperties(uploadFilesDto, mediaFiles);
        mediaFiles.setCompanyId(companyId);
        mediaFiles.setBucket(bucket);
        mediaFiles.setFilePath(object);
        mediaFiles.setFileId(fileMd5);
        mediaFiles.setId(fileMd5);
        mediaFiles.setStatus("1");
        mediaFiles.setAuditStatus(ObjectAuditStatus.NOT_AUDIT);
        String url = "/" + bucket +"/" +object;
        mediaFiles.setUrl(url);
        save(mediaFiles);

        //添加待处理任务
        addWaitingTask(mediaFiles);

        return mediaFiles;
    }

    @Transactional
    @Override
    public UploadFilesVo uploadFiles(Long companyId, MultipartFile file,String htmlObject) throws Exception {
       //保证分布式事务静态页面上传minio的幂等性


        //构造Dto
        long fileSize = file.getSize();
        String originalFilename = file.getOriginalFilename();
        UploadFilesDto uploadFilesDto= UploadFilesDto.builder()
                .fileType(FileType.image)
                .fileSize(fileSize)
                .filename(originalFilename)
                .tags("课程图片")
                .build();
        if(htmlObject!=null){
            uploadFilesDto.setFilename(htmlObject.substring(htmlObject.lastIndexOf('/')+1));
            uploadFilesDto.setFileType(FileType.other);
            uploadFilesDto.setTags("静态页面");
        }

        //获取minio相关参数,并上传
        //操作时的文件路径（无法接受数据，只能接受路径），桶名称，object完整名称，mimetype


        //由于MultipartFile只传输数据，minio需要路径定位资源，再上传
        //因此在本地创建一个临时文件用于上传minio
        File tempFile = File.createTempFile("tempFile", ".tmp");
        file.transferTo(tempFile);
        String localFilePath = tempFile.getAbsolutePath();

        //object完整名称
        String fileName = uploadFilesDto.getFilename();
        String extension = fileName.substring(fileName.lastIndexOf('.'));
        String objectPath = getDefaultObjectPath();
        String objectName = getFileMd5(new File(localFilePath));
        String object = null;
        if(htmlObject == null){
             object = objectPath + objectName + extension;
        }else {
             object = htmlObject;
        }


        //获取mimetype
        String mimeType = getMimeType(extension);

        //上传至minio
        uploadFilesToMinio(localFilePath,mimeType, bucketFiles,object);

        //删除临时文件
        tempFile.delete();



        //对照数据库，看除了DTO，还需要获取数据库哪些其他字段，并上传
        MediaFiles mediaFiles = mediaFilesServiceSelf.uploadFilesToDb(uploadFilesDto, companyId, objectName, bucketFiles, object);

        //构造Vo返回
        UploadFilesVo uploadFilesVo = new UploadFilesVo();
        BeanUtils.copyProperties(mediaFiles,uploadFilesVo);
        return uploadFilesVo;
    }


    @Override
    public boolean checkFile(String md5){
        //查询数据库
        try {
            MediaFiles mediaFiles = getById(md5);
            String object = getDefaultObjectPath(md5);
            if(mediaFiles == null){
                FilterInputStream inputStream = getObjectFromMinio(bucketVideo,object);
                if(inputStream!=null) return true;
                else return false;
            }else return true;//数据库存在，minio一定存在
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean checkBlock(String md5,int chunk){
        try {
            String blockObjectPath = getBlockObjectPath(md5)+chunk;
            FilterInputStream inputStream = getObjectFromMinio(bucketVideo,blockObjectPath);
            return inputStream != null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public boolean uploadBlockFiles(MultipartFile file,String fileMd5,int chunk){
        try {
            File tempFile = File.createTempFile("tempFile", ".tmp");
            file.transferTo(tempFile);
            String mimeType = getMimeType(null);
            String objectPath = getBlockObjectPath(fileMd5);
            String object = objectPath + chunk;
            uploadFilesToMinio(tempFile.getAbsolutePath(),mimeType,bucketVideo,object);
            tempFile.delete();
            return true;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private String getMinioMd5(String bucket,String objectName){

        try {
           InputStream inputStream = getObjectFromMinio(bucket,objectName);
           return DigestUtils.md5Hex(inputStream);
        } catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    private Long getMinioSize(String bucket,String objectName){
        GetObjectResponse response = getObjectFromMinio(bucket, objectName);
        return Long.valueOf(response.headers().get("Content-Length"));
    }

    @Override
    @Transactional
    public void mergerBlockFiles(String fileMd5,String fileName,int totalChunk,Long companyId){
        try {
            //合并minio文件

            //构造组成源集合
            List<ComposeSource> sourceList = new ArrayList<>();
            String blockPath =  getBlockObjectPath(fileMd5);

            for (int i = 0; i < totalChunk; i++) {
                // 构建每个分片的对象名称
                String partObjectName = blockPath+i;
                log.info("每个分片的对象名称为:{}",partObjectName);
                // 创建 ComposeSource 对象
                ComposeSource source = ComposeSource.builder()
                        .bucket(bucketVideo)
                        .object(partObjectName)
                        .build();

                sourceList.add(source);
            }

            //组成对象
            String extension = fileName.substring(fileName.lastIndexOf("."));
            String objectName = getMergeObject(fileMd5,extension) ;
            minioClient.composeObject(ComposeObjectArgs.builder()
                            .bucket(bucketVideo)
                            .object(objectName)
                            .sources(sourceList)
                    .build());

            //判断合并后是否与原文件md5相同
            if (!getMinioMd5(bucketVideo, objectName).equals(fileMd5)){
                log.error("原md5:{},现md5:{}",fileMd5,getMinioMd5(bucketVideo, objectName));
                LearnHubException.throwMyException(fileName+"MD5值不一致");
            }



            //入库
            UploadFilesDto uploadFilesDto = UploadFilesDto.builder()
                    .fileType(FileType.media)
                    .filename(fileName)
                    .tags("课程视频")
                    .fileSize(getMinioSize(bucketVideo,objectName))
                    .build();
            MediaFiles mediaFiles = mediaFilesServiceSelf.uploadFilesToDb(uploadFilesDto, companyId, fileMd5, bucketVideo, objectName);
            if(mediaFiles == null)
                LearnHubException.throwMyException("入库失败");


            //清理分块文件
            deletePartFiles(bucketVideo,fileMd5,totalChunk);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    // 删除分片文件
    private void deletePartFiles(String bucketName, String fileMd5, int totalChunk) {
        for (int i = 0; i < totalChunk; i++) {
            String partObjectName = getBlockObjectPath(fileMd5)+i;
            try {
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                                .bucket(bucketName)
                                .object(partObjectName)
                                .build()
                );
            } catch (Exception e) {
                log.warn("Delete part file failed: " + partObjectName, e);
            }
        }
    }

    //判断是否需要转码
    //@Value("tao.change.mimetype")
    //private final List<String> changeMimetype = new ArrayList<>();
    //根据拓展名判断无法涵盖所有类型，可根据mimetype进行判断
    private boolean isChange(String mimetype){
        List<String> changeMimetype = new ArrayList<>();
        changeMimetype.add("video/x-msvideo");
        log.info("类型为:{}",mimetype);
        if(changeMimetype.contains(mimetype)) {
            return true;
        } else return false;
        //以后若是有多个需要转码的后缀，配置到配置文件中，提取属性到数组，然后在这判断是否在数组
    }

    private void addWaitingTask(MediaFiles mediaFiles){
        //判断是否需要转换
        String fileName = mediaFiles.getFilename();
        String extension = fileName.substring(fileName.lastIndexOf('.'));
        String mimeType = getMimeType(extension);
        if (! isChange(mimeType)) {
            log.info("无需进行转码");
            return;
        }
        log.info("需要进行转码");

        //添加待处理任务
        log.info("开始添加待处理任务.........");
        MediaProcess mediaProcess = new MediaProcess();
        BeanUtils.copyProperties(mediaFiles,mediaProcess);
        //注意，这俩的status代表含义不一样，需要单独修改
        mediaProcess.setStatus("1");
        //上传时间也不一样，不过填充时会自动覆盖
        //id也不一样，但是由于两者类型不同，copy不会正常copy，即copy后的id为null，可以正常插入

        Db.save(mediaProcess);

    }
}
